# 玩具 vite

# 环境准备

git clone  https://github.com/vuejs/vue-dev-server

# package.json

  "bin": {
    "vue-dev-server": "./bin/vue-dev-server.js"
  },
  "scripts": {
    "test": "cd test && node ../bin/vue-dev-server.js"
  },
  "dependencies": {
    "@vue/component-compiler": "^3.6.0",
    "express": "^4.16.4",
    "lru-cache": "^5.1.1",
    "recast": "^0.17.3",
    "validate-npm-package-name": "^3.0.0",
    "vue": "^2.6.8",
    "vue-template-compiler": "^2.6.8"
  }
  • express: 一个nodejs的服务端框架
  • recast: 好像是一个可以对js进行任何操作的库?
  • lru-cache: 一个最近最先使用的存储
  • validate-npm-package-name: 检验npm包名的合法性
  • vue-template-compiler:好像是用于vue2.0预编译的渲染函数
  • @vue/component-compiler: 用于组件包的编译,现已废弃

# bin/vue-dev-server.js

#!/usr/bin/env node

const express = require('express')
const { vueMiddleware } = require('../middleware')

const app = express()
const root = process.cwd();

// app.use  https://www.expressjs.com.cn/4x/api.html#app.use
app.use(vueMiddleware())

app.use(express.static(root))

app.listen(3000, () => {
  console.log('server running at http://localhost:3000')
})

# middleware.js

主要函数体:

// 引入三方包
const vueCompiler = require('@vue/component-compiler')
const fs = require('fs')
const stat = require('util').promisify(fs.stat)
// 返回 Node.js 进程的当前工作目录。
const root = process.cwd()
const path = require('path')
const parseUrl = require('parseurl')
const { transformModuleImports } = require('./transformModuleImports')
const { loadPkg } = require('./loadPkg')
const { readSource } = require('./readSource')

const defaultOptions = {
  cache: true
}
// 如果没有传入参数,则使用默认配置
const vueMiddleware = (options = defaultOptions) => {
  let cache
  let time = {}
  if (options.cache) {
    const LRU = require('lru-cache')

    cache = new LRU({
      max: 500,
      length: function (n, key) { return n * 2 + key.length }
    })
  }

  const compiler = vueCompiler.createDefaultCompiler()
  return async (req, res, next) => {
        // 对请求的路径进行判断,并根据对应的判断进行处理

        // vue文件
        if (req.path.endsWith('.vue')) {      
        const key = parseUrl(req).pathname
        // 尝试获取缓存中的数据
        let out = await tryCache(key)

        if (!out) {
            // Bundle Single-File Component
            const result = await bundleSFC(req)
            out = result
            cacheData(key, out, result.updateTime)
        }
        
        send(res, out.code, 'application/javascript')
        // js 文件
        } else if (req.path.endsWith('.js')) {
        const key = parseUrl(req).pathname
        let out = await tryCache(key)

        if (!out) {
            // transform import statements
            const result = await readSource(req)
            out = transformModuleImports(result.source)
            cacheData(key, out, result.updateTime)
        }

        send(res, out, 'application/javascript')
        // modules
        } else if (req.path.startsWith('/__modules/')) {
        const key = parseUrl(req).pathname
        const pkg = req.path.replace(/^\/__modules\//, '')

        let out = await tryCache(key, false) // Do not outdate modules
        if (!out) {
            out = (await loadPkg(pkg)).toString()
            cacheData(key, out, false) // Do not outdate modules
        }

        send(res, out, 'application/javascript')
        } else {
        next()
        }
    }

整个中间件函数,主要是对vue文件,js文件, modules等文件进行处理。

# tryCache

检查 cache中的数据已经发生更新,如果发生变更返回null,如果没有发生更新,则返回缓存中的数据。

  async function tryCache (key, checkUpdateTime = true) {
    const data = cache.get(key)

    if (checkUpdateTime) {
      const cacheUpdateTime = time[key]
      const fileUpdateTime = (await stat(path.resolve(root, key.replace(/^\//, '')))).mtime.getTime()
      if (cacheUpdateTime < fileUpdateTime) return null
    }

    return data
  }

# bundleSFC 函数

用于编译单文件组件

  async function bundleSFC (req) {
    const { filepath, source, updateTime } = await readSource(req)
    const descriptorResult = compiler.compileToDescriptor(filepath, source)
    const assembledResult = vueCompiler.assemble(compiler, filepath, {
      ...descriptorResult,
      script: injectSourceMapToScript(descriptorResult.script),
      styles: injectSourceMapsToStyles(descriptorResult.styles)
    })
    return { ...assembledResult, updateTime }
  }

# readSource

从 req中拿到 pathname,然后返回这个文件的路径和内容,和最新的更新时间。

const path = require('path')
const fs = require('fs')
const readFile = require('util').promisify(fs.readFile)
const stat = require('util').promisify(fs.stat)
const parseUrl = require('parseurl')
const root = process.cwd()

async function readSource(req) {
  const { pathname } = parseUrl(req)
  const filepath = path.resolve(root, pathname.replace(/^\//, ''))
  return {
    filepath,
    source: await readFile(filepath, 'utf-8'),
    updateTime: (await stat(filepath)).mtime.getTime()
  }
}
exports.readSource = readSource

# cacheData

从chache中拿到已缓存的数据,进行新旧对比,如果不同,则对应的更新内容和时间。如果相同,则返回false

 function cacheData (key, data, updateTime) {
    const old = cache.peek(key)

    if (old != data) {
      cache.set(key, data)
      if (updateTime) time[key] = updateTime
      return true
    } else return false
  }

# send函数

function send(res, source, mime) {
    res.setHeader('Content-Type', mime)
    // https://www.expressjs.com.cn/4x/api.html#res.end
    // 结束响应过程。
    res.end(source)
}

# transformModuleImports

修改module包的引入方式,同时得到包里的代码

const recast = require('recast')
const isPkg = require('validate-npm-package-name')

function transformModuleImports(code) {
  const ast = recast.parse(code)
  recast.types.visit(ast, {
    visitImportDeclaration(path) {
      const source = path.node.source.value
      if (!/^\.\/?/.test(source) && isPkg(source)) {
        path.node.source = recast.types.builders.literal(`/__modules/${source}`)
      }
      this.traverse(path)
    }
  })
  return recast.print(ast).code
}

exports.transformModuleImports = transformModuleImports

# loadPkg

将三方包转义成 浏览器可以直接使用的方式,此时的代码只支持vue包。

const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)

async function loadPkg(pkg) {
  if (pkg === 'vue') {
    const dir = path.dirname(require.resolve('vue'))
    const filepath = path.join(dir, 'vue.esm.browser.js')
    return readFile(filepath)
  }
  else {
    // TODO
    // check if the package has a browser es module that can be used
    // otherwise bundle it with rollup on the fly?
    throw new Error('npm imports support are not ready yet.')
  }
}

exports.loadPkg = loadPkg

# 参考资料